/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.impl.llom.OMSourcedElementImpl;

import at.ac.tuwien.infosys.dsg.daios.exceptions.ArrayException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedClassException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.AtomicTypesMapper;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLType;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLPart;
import at.ac.tuwien.infosys.dsg.daios.wsdl.typesystem.WSDLTypeTree;

/**
 * A concrete subclass of OMElementConverter. This is the concrete
 * instance used to convert messages in 'doc/wrapped' style invocations.
 * 
 * <p>
 * 
 * This converter transforms Daios Messages to AXIOM models (as
 * used by Apache Axis2) and vice versa.
 * 
 * @author philipp
 *
 */
public class OMDocWrappedConverter extends OMElementConverter {
	
	private IWSDLType wrapperType = null;
	
	/**
	 * Private constructor. Create a new converter instance to
	 * a given WSDL output message.
	 * 
	 *  <p>
	 *  
	 *  Internally this constructor finds the <i>parameter
	 *  wrapper type</i> and saves it.
	 * 
	 * @param output The WSDL output message to use in the
	 * conversion.
	 * @param types The WSDL type tree.
	 */
	private OMDocWrappedConverter(IWSDLMessage output, WSDLTypeTree types) {
		log = Logger.getLogger(IConstants.LOG_NAME);
		fac = OMAbstractFactory.getOMFactory();
		sb = new StringBuilder();
		
		// check if we have a oneway invocation
		if(output == null)
			return;
		
		List<IWSDLPart> l = output.getParts();
		IWSDLPart part = l.get(0);
		String partType = part.getPartType();
		wrapperType = 
			types.getGlobalElement(partType);
		
	}

	/**
	 * Private constructor. Create a new converter instance to
	 * a given wrapper type.
	 * 
	 * @param type The wrapper type as defined by doc/wrapped.
	 */	
	private OMDocWrappedConverter(IWSDLType type) {
		log = Logger.getLogger(IConstants.LOG_NAME);
		fac = OMAbstractFactory.getOMFactory();
		wrapperType = type;
	}
	
	/**
	 * Factory method. Create a new instance of OMDocWrappedConverter. 
	 * 
	 * @param output The WSDL output message to use in the
	 * conversion.
	 * @param types The WSDL type tree.
	 * @return The new converter instance.
	 */
	protected static OMDocWrappedConverter getInstance(IWSDLMessage output,
			WSDLTypeTree types) {
		return new OMDocWrappedConverter(output, types);
	}
	
	/**
	 * Converts a Daios message to AXIOM using the given
	 * WSDL operation and WSDL input message.
	 * 
	 * @param op The WSDL operation to use in the invocation.
	 * @param msg The WSDL message to use in the invocation.
	 * @param actualMsg The Daios message that provides the
	 * acutal user-delivered input data.
	 * @param types A tree containing all types from the WSDL
	 * definition.
	 * 
	 */
	public OMElement convert(IWSDLOperation op, IWSDLMessage msg,
			DaiosInputMessage actualMsg, WSDLTypeTree types) {

		// if this is an empty message return the root with NULL namespace
		if(msg.getParts().size() == 0)
			return fac.createOMElement(op.getName(), null);
		
		// find the wrapper type
		IWSDLPart wrapperPart = msg.getParts().get(0);
		String typeName = wrapperPart.getPartType();
		IWSDLType wrapper = types.getGlobalElement(typeName);
		
		// create root element
		OMElement root = null; 
		
		root = addOMPart(wrapper.getName().getLocalPart(), actualMsg,
				wrapper.getName().getNamespaceURI(),
				wrapper.getName().getPrefix(), wrapper);
		
//		OMNamespace ns = fac.createOMNamespace(wrapper.getName().getNamespaceURI(),
//				wrapper.getName().getPrefix());
//		root = new OMSourcedElementImpl(wrapper.getName().getLocalPart(),
//				ns, fac, new DaiosDataSource(actualMsg,
//								wrapper.getName().getLocalPart(),
//								wrapper.getName().getNamespaceURI(),
//								wrapper.getName().getPrefix(),
//								wrapper, this));
		
		return root;
	}

	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage according to the
	 * output message associated with this converter.
	 * 
	 * @param returnMessage The XML response as OM model.
	 * @return The XML document converted to a DaiosOutputMessage.
	 */
	public DaiosOutputMessage convert(OMElement returnMessage) {
		return convert(returnMessage, 0);
	}

	/**
	 * Convert an asynchronously received document into a Daios message.
	 * For some reason we sometimes receive different messages from
	 * Axis2 depending on whether we use synchronous or asynchronous
	 * invocations. This might be a bug in Axis2.
	 * 
	 * <p>
	 * 
	 * In this case we <i>can</i> reuse the standard conversion
	 * logics, but we have to normalize the AXIOM model a little
	 * bit in advance.
	 * 
	 * @param result The asynchronous result to convert
	 * @return The converted Daios message.
	 */
	public DaiosOutputMessage convertAsync(OMElement result) {
		
		// we have to go to the body ...
		Iterator it = result.getChildElements();
		OMElement next = null;
		while(it.hasNext()) {
			next = (OMElement)it.next();
			if(next.getQName().getLocalPart().equalsIgnoreCase("Body"))
				break;
		}
		
		// and down another level ...
		it = next.getChildElements();
		next = (OMElement)it.next();
		
		// now we can reuse the standard OM converter logic
		return convert(next, 0);
		
	}
	
	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage according to the
	 * output message associated with this converter.
	 * 
	 * @param returnMessage The XML response as OM model.
	 * @param seqNr A sequence number indicating the current
	 * position in the wrapper types' 'sub type sequence'.
	 * @return The XML document converted to a DaiosOutputMessage.
	 */
	private DaiosOutputMessage convert(OMElement returnMessage, int seqNr) {
		
		DaiosOutputMessage m = new DaiosOutputMessage();
		
		Iterator it = returnMessage.getChildren();
		int usedCounter = 0;
		Object o = null;
		OMElement el = null;
		boolean isSimple = false;
		boolean isArray = false;
		while(it.hasNext()) {
			o = it.next();
			if(o instanceof OMElement)
				el = (OMElement) o;
			else {
				//  ignore text content
				continue;
			}
			if(usedCounter > 1) {
				usedCounter--;
				if(it.hasNext())
					it.next();
				continue;
			}
			isSimple = checkIfSimple(el);
			isArray = checkIfArray(el);
			// we have a simple type
			if(isSimple && !isArray) {
				extractSimpleValue(m, el, seqNr);
			}
			// we have an array of simple types
			else if(isSimple && isArray) {
				usedCounter = extractArrayValues(
					m, el, seqNr);
			}
			// we have a complex type
			else if(!isSimple && !isArray){
				extractComplexValue(m, el, seqNr);
			}
			// we have a complex type array
			else {
				usedCounter = extractComplexArrayValues(
						m, el, seqNr);				
			}
			
			// increment the sequence number
			seqNr++;
			
		}
		
		return m;
		
	}
	
	/**
	 * Extract a simple value from the XML model and insert it into
	 * the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple value as AXIOM model.
	 * @param seqNr The current sequence number.
	 */
	private void extractSimpleValue(DaiosOutputMessage m,
			OMElement el, int seqNr) {
		
		try {
			
			String type =
				wrapperType.getSequence()[seqNr].getTypeName().getLocalPart();
			String val = el.getText();
			Class vClass = AtomicTypesMapper.get(type);
			Object castedVal
				= AtomicTypesMapper.construct(val,vClass);
			
			m.set(el.getLocalName(),castedVal,vClass);
			
		} catch (IOException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (UnsupportedClassException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (TypeErrorException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Extract an array of simple values from the XML model and insert them
	 * as array value into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple array value as AXIOM model.
	 * @param seqNr The current sequence number.
	 * @return The number of array values extracted.
	 */
	private int extractArrayValues(DaiosOutputMessage m,
			OMElement el, int seqNr) {
		
		List<String> l = new LinkedList<String>();
		
		String type =
			wrapperType.getSequence()[seqNr].getTypeName().getLocalPart();
		String name = el.getLocalName();

		int elCounter = 0;
		
		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			
			elCounter++;
			
			String val = e.getText();
			
			// ignore empty array values
			if(val == null || val.trim().equals(""))
				continue;
			
			l.add(val);
		}
		
		// now add this array to the message
		try {
			m.set(name, convert(l, type),
				AtomicTypesMapper.getArray(type));
		} catch (TypeErrorException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		}
		
		// return the number of elements we have used
		// (so that they are not used again)
		return elCounter;
	}
	
	/**
	 * Extract an array of complex values from the XML model and insert them
	 * as array of DaiosMessages into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex array value as AXIOM model.
	 * @param seqNr The current sequence number.
	 * @return The number of array elements extracted.
	 */	
	private int extractComplexArrayValues(DaiosOutputMessage m,
			OMElement el, int seqNr) {
		
		List<DaiosMessage> l = new LinkedList<DaiosMessage>();
		
		String name = el.getLocalName();
		
		int elCounter = 0;
		
		// get all array values in a list
		DaiosMessage val = null;
		OMDocWrappedConverter converter = new OMDocWrappedConverter(
			wrapperType.getSequence()[seqNr]);
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			elCounter++;
			val = converter.convert(e, 0);
			l.add(val);
		}
		
		// now add this array to the message
		try {
			m.setComplexArray(name, convert(l));
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		}
		
		// return the number of elements we have used
		// (so that they are not used again)
		return elCounter;
		
	}
	
	/**
	 * Extract a complex value from the XML model and insert it
	 * as DaiosMessage into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex value as AXIOM model.
	 * @param seqNr The current sequence number.
	 */		
	private void extractComplexValue(DaiosOutputMessage m,
			OMElement el, int seqNr) {
		// recursively go deeper
		OMDocWrappedConverter converter = new OMDocWrappedConverter(
			wrapperType.getSequence()[seqNr]);
		m.setComplex(el.getLocalName(), converter.convert(el, 0));
	}

	/**
	 * Check if a certain AXIOM construct represents a simple value.
	 * 
	 * <p>
	 * 
	 * We consider a value 'simple' if it has no attributes and
	 * no subelements.
	 * 
	 * 
	 * @param el The element to check.
	 * @return True if the element is simple, false otherwise.
	 */	
	private boolean checkIfSimple(OMElement el) {
		
		if(el.getFirstElement() != null)
			return false;
		
		if(el.getAllAttributes().hasNext())
			return false;
		
		return true;
		
	}

	/**
	 * Check if a certain AXIOM construct represents an array value.
	 * 
	 * <p>
	 * 
	 * An element is considered an 'array' if it has a
	 * previous <i>or</i> a next OM sibling with the same
	 * name.
	 * 
	 * @param el The element to check.
	 * @return True if the element is an array, false otherwise.
	 */
	private boolean checkIfArray(OMElement el) {
		
		String name = el.getLocalName();
		
		OMElement previous = OMUtil.getPreviousOMElement(el);
		if(previous != null && previous.getLocalName().equals(name))
			return true;
		
		OMElement next = OMUtil.getNextOMElement(el);
		if(next != null && next.getLocalName().equals(name))
			return true;
		
		return false;
	}
	
}
